package com.packenius.library.xspdf;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.imageio.ImageIO;

/**
 * Central place for creating a PDF document or stream.
 * @author Christian Packenius, 2013.
 */
public final class XSPDF {
  /**
   * Version of the new PDF document.
   */
  String pdfVersion = "1.5";

  /**
   * List of all pages of this PDF document.
   */
  List<XSPage> pages = new ArrayList<XSPage>();

  /**
   * Current PDF page. All new additions to this PDF document will be written on
   * this site of the document if it fits.
   */
  XSPage currentPage;

  /**
   * Page fontSize for every new page.
   */
  private XSPageSize currentPageSize = XS.DIN_A4;

  private static final XSFontType[] HelveticaFonts = new XSFontType[] { XSFontType.Helvetica, XSFontType.HelveticaBold,
      XSFontType.HelveticaOblique, XSFontType.HelveticaBoldOblique };

  private static final XSFontType[] CourierFonts = new XSFontType[] { XSFontType.Courier, XSFontType.CourierBold,
      XSFontType.CourierOblique, XSFontType.CourierBoldOblique };

  private static final XSFontType[] TimesFonts = new XSFontType[] { XSFontType.Times, XSFontType.TimesBold,
      XSFontType.TimesItalic, XSFontType.TimesBoldItalic };

  /**
   * Currently used font type.
   */
  private XSFontType currentFontType = XS.TIMES;

  /**
   * Font parameters (bold, italic). "ORDINARY" should never be content in this
   * set!
   */
  private Set<XSFontParameter> currentFontParms = new HashSet<XSFontParameter>();

  /**
   * Currently used font size.
   */
  private double currentFontSize = 12.0;

  /**
   * Current text alignment for coming text prints.
   */
  private XSAlignment currentAlignment = XS.JUSTIFICATION;

  /**
   * Line leading for following text lines and pages.
   */
  XSLineLeading currentLineLeading = new XSLineLeading(currentFontSize / 2.0);

  /**
   * Encoding which is used for packing page content.
   */
  XSContentEncoding contentEncoding = XSContentEncoding.DeflateEncoding;

  /**
   * Text fill color to use for the following text.
   */
  private Color currentTextFillColor = Color.BLACK;

  /**
   * Text stroke color to use for the following text.
   */
  private Color currentTextStrokeColor = Color.BLACK;

  /**
   * All contentListeners of the document content.
   */
  Set<XSContentListener> contentListeners = new HashSet<XSContentListener>();

  /**
   * Number of spaces to indent the first line of a text paragraph if it is left
   * aligned or justified.
   */
  double currentTextParagraphIndentationSpaces = 2;

  /**
   * Last printed text part.
   */
  String lastTextPart = "\n";

  /**
   * Last printed/filled text line.
   */
  XSTextLine lastTextLine = null;

  /**
   * How to align last line when using full justification alignment.
   */
  boolean lastLineRightAligned = false;

  /**
   * How to render text (fill, stroke or both).
   */
  private XSTextRenderingMode currentTextRenderMode = XSTextRenderingMode.Fill;

  /**
   * Auto formatting (double space removal, ...) or no formatting.
   */
  private XSTextFormattingMode currentFormattingMode = XSTextFormattingMode.AutoFormat;

  /**
   * Maps a virtual key to an image byte stream.
   */
  Map<String, byte[]> images = new HashMap<String, byte[]>();

  /**
   * Maps the virtual image key to the PDF Object ID.
   */
  Map<String, Integer> imageObjectIDs = new HashMap<String, Integer>();

  /**
   * Contains the external images which are included via their URL.
   */
  List<URL> imageURLs = new ArrayList<URL>();

  /**
   * This columns will be added to new pages.
   */
  List<XSColumn> standardColumns = new ArrayList<XSColumn>();

  /**
   * Page margin. This will be used when creating the first column for a page.
   */
  private XSPageMargin currentPageMargin = new XSPageMargin(XSStatics.convertToUserUnits(20.0, XS.MM));

  /**
   * Factor for all "length values".
   */
  double currentUnitFactor = 1.0;

  /**
   * Object for translating use() method calls.
   */
  private final XSUse xsUse;

  /**
   * PageMode for the whole document, for example FullScreen.
   */
  XSPageMode pageMode = XSPageMode.ShowDocumentOnly;

  /**
   * Page background color for following pages.
   */
  private Color currentPageBackgroundColor = Color.WHITE;

  /**
   * Column background color for following pages.
   */
  Color currentColumnBackgroundColor = null;

  /**
   * All new columns will get this layer ID.
   */
  int currentLayerID = 0;

  /**
   * Default presentation duration for any page. If zero or negative, there is
   * no presentation mode enabled.
   */
  double presentationDurationPerPage = 0.0;

  /**
   * Transition mode in Slideshow (Presentation mode / fullscreen only) for
   * current and following pages.
   */
  XSTransitionMode currentTransitionMode = XS.DEFAULT_TRANSITION_MODE;

  /**
   * Page label for first page. (This value is only filled if the first page not
   * yet have been created.
   */
  private XSPageLabel currentPageLabel = null;

  /**
   * This object (destination name (String) or URL) should be used for a link
   * generated via print() or setImage().
   */
  private XSLink currentLink;

  /**
   * Number of print() or setImage() calls to set the upper destination name as
   * link. If this value is zero, the end of link setting must be given via
   * stopLink.
   */
  private int currentLinkCount;

  /**
   * List of all destinations within the document.
   */
  Map<XSLink, XSDestination> destinations = new HashMap<XSLink, XSDestination>();

  /**
   * Thickness of the border of following links in user units.
   */
  private double currentLinkBorderSize;

  /**
   * Instanciates a new PDF document.
   * @return New XSPDF object.
   */
  public static XSPDF getInstance() {
    return new XSPDF();
  }

  /**
   * Constructor.
   */
  public XSPDF() {
    xsUse = new XSUse(this);
  }

  /**
   * Clears the document starting with a single blank page. All parameters
   * (current font, page size, encoding, ...) will stay unchanged.
   * @return This PDF document.
   */
  public XSPDF deleteAllPages() {
    pages.clear();
    return this;
  }

  /**
   * Creates another page at the end of this PDF document.
   * @return This PDF document.
   */
  public XSPDF newPage() {
    if (currentPage != null) {
      currentPage.setPageEnd();
    }
    lastTextPart = "\n";
    newPageInternal();
    return this;
  }

  void newPageInternal() {
    currentPage = new XSPage(this, pages.size() + 1, currentPageSize, currentPageMargin, currentPageBackgroundColor,
        currentTransitionMode, presentationDurationPerPage, currentPageLabel);
    currentPageLabel = null;
    pages.add(currentPage);
    for (XSContentListener listener : contentListeners) {
      listener.newPage(this, pages.size());
    }
  }

  /**
   * Creates a new PDf file.
   * @param filename File name.
   * @return This PDF document.
   * @throws IOException
   */
  public XSPDF createPdf(String filename) throws IOException {
    createPdf(new File(filename));
    return this;
  }

  /**
   * Creates a new PDf file.
   * @param pdfFile PDF file to create. Will be overwritten if it exists.
   * @return This PDF document.
   * @throws IOException
   */
  public XSPDF createPdf(File pdfFile) throws IOException {
    BufferedOutputStream out = null;
    try {
      File parent = pdfFile.getCanonicalFile().getParentFile();
      if (parent != null && !parent.exists()) {
        pdfFile.getParentFile().mkdirs();
      }
      out = new BufferedOutputStream(new FileOutputStream(pdfFile));
      createPdf(out);
    } finally {
      if (out != null) {
        out.close();
      }
    }
    return this;
  }

  /**
   * Creates a new PDf stream.
   * @param out Output stream for writing PDF stream.
   * @return This PDF document.
   * @throws IOException
   */
  public XSPDF createPdf(OutputStream out) throws IOException {
    new XSCreator(out, this).create();
    return this;
  }

  /**
   * Prints a single character into the PDF document.
   * @param character Character to print.
   * @return This object.
   */
  public XSPDF print(char character) {
    return print("" + character);
  }

  /**
   * Prints some text into the PDF document.
   * @param text Text to print.
   * @return This object.
   */
  public XSPDF print(String text) {
    text = cleanUpText(text);
    List<String> textParts = splitTextIntoParts(text);
    XSLink link = getDestinationForLink();
    for (String part : textParts) {
      // Do not set the following line before the for loop - the font usage can
      // change with every text part because of
      // the content listeners!
      XSFontUsage currentFontUsage = getCurrentFontUsage(); // MUST BE HERE!

      if (currentPage == null) {
        newPageInternal();
      }

      if (!currentPage.addTextPart(part, currentFontUsage, link)) {
        currentFontUsage = getCurrentFontUsage(); // MUST BE HERE!
        newPageInternal();
        // Yes, the current font usage can be changed in the two lines before!
        currentFontUsage = getCurrentFontUsage(); // MUST BE HERE!
        if (!currentPage.addTextPart(part, currentFontUsage, link)) {
          throw new XSPdfException("Can't print text on page: <" + part + ">");
        }
      }
      lastTextPart = part;
    }
    return this;
  }

  private XSLink getDestinationForLink() {
    XSLink link = currentLink;
    if (currentLinkCount > 0) {
      currentLinkCount--;
      if (currentLinkCount == 0) {
        currentLink = null;
      }
    }
    if (link != null && destinations.get(link) == null) {
      destinations.put(link, null);
    }
    return link;
  }

  /**
   * Only for internal usage, please!!!
   */
  XSFontUsage getCurrentFontUsage() {
    int fontParm = 0;
    fontParm |= currentFontParms.contains(XSFontParameter.Bold) ? 1 : 0;
    fontParm |= currentFontParms.contains(XSFontParameter.Italic) ? 2 : 0;
    XSFontType fontType = null;
    switch (currentFontType) {
    case Helvetica:
      fontType = HelveticaFonts[fontParm];
      break;
    case Courier:
      fontType = CourierFonts[fontParm];
      break;
    case Times:
      fontType = TimesFonts[fontParm];
      break;
    case Symbol:
      fontType = XS.SYMBOL;
      break;
    case ZapfDingbats:
      fontType = XS.ZAPFDINGBATS;
      break;
    default:
      throw new XSPdfException("Illegal font usage <" + currentFontType.fontName + ">!");
    }
    return new XSFontUsage(fontType, currentFontSize, currentAlignment, currentTextFillColor, currentTextStrokeColor,
        currentTextRenderMode, currentFormattingMode);
  }

  private String cleanUpText(String text) {
    switch (currentFormattingMode) {
    case AutoFormat:
      text = trimSpaces(text);
      text = replaceDoubleSpaces(text);
      break;
    case NoFormatting:
      break;
    }
    text = replaceUnwantedLineBreakers(text);
    return text;
  }

  /**
   * Delete only spaces at begin and end of the given string.
   */
  private String trimSpaces(String text) {
    int length = text.length();
    int k = 0, m = length - 1;
    char[] ca = text.toCharArray();
    while (k < length && ca[k] == ' ') {
      k++;
    }
    while (m >= k && ca[m] == ' ') {
      m--;
    }
    return text.substring(k, m + 1);
  }

  /**
   * Change all double spaces to single spaces.
   */
  private String replaceDoubleSpaces(String text) {
    int oldSize = text.length();
    while (true) {
      text = text.replace("  ", " ");
      int newSize = text.length();
      if (newSize == oldSize) {
        break;
      }
      oldSize = newSize;
    }
    return text;
  }

  /**
   * Change line breaking characters in the way that only \n exists in the
   * string.
   */
  private String replaceUnwantedLineBreakers(String text) {
    text = text.replace("\r\n", "\n");
    text = text.replace("\r", "\n");
    return text;
  }

  private List<String> splitTextIntoParts(String text) {
    List<String> parts = new ArrayList<String>();
    char[] chars = text.toCharArray();
    int textLength = text.length();
    int start = 0;
    boolean noFormatting = currentFormattingMode == XSTextFormattingMode.NoFormatting;
    for (int i = 0; i < textLength; i++) {
      char ch = chars[i];
      if (ch == '\n' || ch == ' ') {
        addPart(parts, text.substring(start, i));
        if (ch == '\n') {
          addPart(parts, "\n");
        } else if (ch == ' ' && noFormatting) {
          addPart(parts, " ");
        }
        start = i + 1;
      }
    }
    addPart(parts, text.substring(start, textLength));
    return parts;
  }

  private void addPart(List<String> parts, String part) {
    if (part.length() > 0) {
      parts.add(part);
    }
  }

  /**
   * Set the unit type to use for following calls.
   * @param unitType USER_UNIT, MM or INCH.
   * @return This PDF document.
   */
  public XSPDF setUnitType(XSUnitType unitType) {
    return setUnitType(unitType, 1.0);
  }

  /**
   * Set the unit type to use for following calls.
   * @param unitType USER_UNIT, MM or INCH.
   * @param factor Factor for upper unit type.
   * @return This PDF document.
   */
  public XSPDF setUnitType(XSUnitType unitType, double factor) {
    currentUnitFactor = XSStatics.convertToUserUnits(factor, unitType);
    return this;
  }

  /**
   * Get the currently factor in UserUnits.
   * @return Currently factor in UserUnits.
   */
  public double getUnitSizeInUserUnits() {
    return currentUnitFactor;
  }

  /**
   * Initializes a new PDF document with the given size.
   * @param width Width in units of unitType.
   * @param height Height in units of unitType.
   * @return New XSPDF object.
   */
  public XSPDF setPageSize(double width, double height) {
    width *= currentUnitFactor;
    height *= currentUnitFactor;
    return setPageSize(new XSPageSize(width, height));
  }

  /**
   * Set a new size for the current and following pages.
   * @param pageSize New page size.
   * @return This PDF document.
   */
  public XSPDF setPageSize(XSPageSize pageSize) {
    currentPageSize = pageSize;
    double dx = pageSize.width / 10.0;
    double dy = pageSize.height / 10.0;
    currentPageMargin = new XSPageMargin(dy, dy, dx, dx, dx / 2.0, dy / 2.0);
    if (currentPage != null) {
      currentPage.setPageSize(currentPageSize);
      currentPage.setPageMargin(currentPageMargin);
    }
    return this;
  }

  /**
   * Returns the current page size.
   * @return Page size of current and/or following pages.
   */
  public XSDimension getPageSize() {
    return new XSDimension(currentPageSize.width, currentPageSize.height);
  }

  /**
   * Set parameters for this PDF document.
   * @param parms Parameters, see interface XS.
   * @return This PDF document.
   * @throws IOException
   */
  public XSPDF use(Object... parms) throws IOException {
    xsUse.use(parms);
    return this;
  }

  /**
   * Set an image at a special place on the current page.
   * @param image BufferedImage object.
   * @param x X coordinate of upper left corner.
   * @param y Y coordinate of upper left corner.
   * @param width Width in user units.
   * @param height Height in user units.
   * @param margin Margin around the image. This is neccessary to avoid text
   *          next to the image.
   * @return This PDF document.
   */
  public XSPDF setImage(BufferedImage image, double x, double y, double width, double height, double margin) {
    if (currentPage == null) {
      newPageInternal();
    }
    ColorModel colorModel = image.getColorModel();
    int numComponents = colorModel.getNumComponents();
    int pixelSize = colorModel.getPixelSize();
    if (numComponents != 3 || pixelSize != 24) {
      throw new XSPdfException("Please use 24 bits per pixel RGB image!");
    }
    x *= currentUnitFactor;
    y *= currentUnitFactor;
    width *= currentUnitFactor;
    height *= currentUnitFactor;
    margin *= currentUnitFactor;
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    try {
      ImageIO.write(image, "jpeg", baos);
    } catch (IOException exception) {
      throw new XSPdfException("Can't get jpeg stream from image! Coding error @setImage?!");
    }
    byte[] ba = baos.toByteArray();
    int realWidth = image.getWidth();
    int realHeight = image.getHeight();
    String imageID = storeImage(ba, realWidth, realHeight);
    XSLink link = getDestinationForLink();
    currentPage.setImage(imageID, x, y, width, height, realWidth, realHeight, margin, link);
    return this;
  }

  private String storeImage(byte[] ba, int realWidth, int realHeight) {
    int length = ba.length;
    String keyPrefix = length + "-" + realWidth + "x" + realHeight + "-";
    for (int i = 0; i >= 0; i++) {
      String key = keyPrefix + i;
      byte[] ba2 = images.get(key);
      if (ba2 == null) {
        images.put(key, ba);
        return key;
      }
      boolean found = true;
      for (int k = 0; k < length; k++) {
        if (ba[k] != ba2[k]) {
          found = false;
          break;
        }
      }
      if (found) {
        return key;
      }
    }

    // We will never reach this point of code (I hope...)!
    throw new XSPdfException("Error in storeImage()!");
  }

  /**
   * Set an image at a special place on the current page. This image must be DCT
   * encoded ("JPEG/JFIF") with RGB colors and 8 bits per plane.
   * @param imageURL URL to the image.
   * @param x X coordinate of upper left corner.
   * @param y Y coordinate of upper left corner.
   * @param realSize Pixel size of the image.
   * @param width Width in user units.
   * @param height Height in user units.
   * @param margin Margin around the image. This is neccessary to avoid text
   *          next to the image.
   * @return This PDF document.
   */
  public XSPDF setImage(URL imageURL, Dimension realSize, double x, double y, double width, double height, double margin) {
    if (currentPage == null) {
      newPageInternal();
    }
    x *= currentUnitFactor;
    y *= currentUnitFactor;
    width *= currentUnitFactor;
    height *= currentUnitFactor;
    margin *= currentUnitFactor;
    int realWidth = realSize.width;
    int realHeight = realSize.height;
    XSLink link = getDestinationForLink();
    String imageID = storeImage(imageURL);
    currentPage.setImage(imageID, x, y, width, height, realWidth, realHeight, margin, link);
    return this;
  }

  private String storeImage(URL imageURL) {
    if (!imageURLs.contains(imageURL)) {
      imageURLs.add(imageURL);
    }
    return "Ext" + imageURLs.indexOf(imageURL);
  }

  /**
   * Set the number of spaces the first line of a text paragraph shall be
   * indented with when the text paragraph is left aligned or justificated.
   * @param textParagraphIndentationSpaceCount Number of spaces to indent with.
   *          This value may be negative to put text into the border.
   * @return This PDF document.
   */
  public XSPDF setTextParagraphIndentationSpaces(int textParagraphIndentationSpaceCount) {
    currentTextParagraphIndentationSpaces = textParagraphIndentationSpaceCount;
    return this;
  }

  /**
   * Returns the current count of spaces before the first line in justified or
   * left aligned text paragraphs.
   * @return Current number of indentation spaces.
   */
  public double getTextParagraphIndentationSpaces() {
    return currentTextParagraphIndentationSpaces;
  }

  /**
   * Set the text fill color for all following text prints.
   * @param textFillColor Text fill color to use.
   * @return This PDF document.
   */
  public XSPDF setTextFillColor(Color textFillColor) {
    currentTextFillColor = textFillColor;
    return this;
  }

  /**
   * Returns the current text filling color.
   * @return Current text filling color.
   */
  public Color getTextFillColor() {
    return currentTextFillColor;
  }

  /**
   * Set the text stroke color for all following text prints.
   * @param textStrokeColor Text stroke color to use.
   * @return This PDF document.
   */
  public XSPDF setTextStrokeColor(Color textStrokeColor) {
    currentTextStrokeColor = textStrokeColor;
    return this;
  }

  /**
   * Returns the current text stroking color.
   * @return Current text stroking color.
   */
  public Color getTextStrokeColor() {
    return currentTextStrokeColor;
  }

  /**
   * Create a PDF file from parms immediatelly.
   * @param file PDF file to create.
   * @param parms PDF parameters.
   * @return This PDF document.
   * @throws IOException
   */
  public static XSPDF create(File file, Object... parms) throws IOException {
    XSPDF xsPDF = getInstance();
    xsPDF.use(parms);
    xsPDF.createPdf(file);
    return xsPDF;
  }

  /**
   * Create a PDF file from parms immediatelly.
   * @param filename PDF file to create.
   * @param parms PDF parameters.
   * @throws IOException
   */
  public static void create(String filename, Object... parms) throws IOException {
    XSPDF xsPDF = getInstance();
    xsPDF.use(parms);
    xsPDF.createPdf(filename);
  }

  /**
   * Create a PDF stream from parms immediatelly.
   * @param pdfStream PDF stream to create.
   * @param parms PDF parameters.
   * @throws IOException
   */
  public static void create(OutputStream pdfStream, Object... parms) throws IOException {
    XSPDF xsPDF = getInstance();
    xsPDF.use(parms);
    xsPDF.createPdf(pdfStream);
  }

  /**
   * Set font family.
   * @param fontType Font type (family).
   * @return This PDF document.
   */
  public XSPDF setFontFamily(XSFontType fontType) {
    if (fontType == null) {
      throw new XSPdfException("Can't set null font type!");
    }
    currentFontType = fontType;
    currentFontParms.clear();
    return this;
  }

  /**
   * Set font size.
   * @param fontSize Font size.
   * @return This PDF document.
   */
  public XSPDF setFontSize(double fontSize) {
    currentFontSize = fontSize * currentUnitFactor;
    setLineLeading(currentFontSize / 2.0 / currentUnitFactor);
    return this;
  }

  /**
   * Set font parameters.
   * @param fontParameters Wanted font parameters.
   * @return This PDF document.
   */
  public XSPDF setFontParameters(XSFontParameter... fontParameters) {
    if (fontParameters != null && fontParameters.length > 0) {
      for (XSFontParameter fontParameter : fontParameters) {
        switch (fontParameter) {
        case Ordinary:
          currentFontParms.clear();
          break;
        default:
          currentFontParms.add(fontParameter);
        }
      }
    }
    return this;
  }

  /**
   * Set font type, size and parameters.
   * @param fontType Font type.
   * @param fontSize Font size.
   * @param fontParameters Wanted font parameters.
   * @return This PDF document.
   */
  public XSPDF setFont(XSFontType fontType, double fontSize, XSFontParameter... fontParameters) {
    setFontFamily(fontType);
    setFontSize(fontSize);
    setFontParameters(fontParameters);
    return this;
  }

  /**
   * Set font type, size and parameters.
   * @param fontType Font type.
   * @param fontParameters Wanted font parameters.
   * @return This PDF document.
   */
  public XSPDF setFont(XSFontType fontType, XSFontParameter... fontParameters) {
    setFontFamily(fontType);
    setFontParameters(fontParameters);
    return this;
  }

  /**
   * Set the type of content encoding for PDF objects.
   * @param contentEncoding Type of content encoding.
   * @return This PDF document.
   */
  public XSPDF setContentEncoding(XSContentEncoding contentEncoding) {
    if (contentEncoding == null) {
      this.contentEncoding = XSContentEncoding.DeflateEncoding;
    } else {
      this.contentEncoding = contentEncoding;
    }
    return this;
  }

  /**
   * Get the content encoding for the whole document.
   * @return Content encoding for this document.
   */
  public XSContentEncoding getContentEncoding() {
    return contentEncoding;
  }

  /**
   * Sets the line leading for following pages.
   * @param value Value of line leading.
   * @return This PDF document.
   */
  public XSPDF setLineLeading(double value) {
    currentLineLeading = new XSLineLeading(value * currentUnitFactor);
    return this;
  }

  /**
   * Sets the line leading for following pages in percent of the font size.
   * @param value Value of line leading in percent of the font size.
   * @return This PDF document.
   */
  public XSPDF setLineLeadingInPercent(double value) {
    currentLineLeading = new XSLineLeading(currentFontSize * value / 100.0);
    return this;
  }

  /**
   * Adds a new content listener to this document creator.
   * @param xsContentListener New content listener.
   * @return This PDF document.
   */
  public XSPDF addContentListener(XSContentListener xsContentListener) {
    contentListeners.add(xsContentListener);

    // Inform if we are on a "blank" page (usually the first page).
    if (currentPage != null) {
      if (!currentPage.hasAnyContent()) {
        xsContentListener.newPage(this, pages.size());
      }
    }

    return this;
  }

  /**
   * Set the mode how to render the text.
   * @param textRenderMode Render mode (fill, stroke or both; default is FILL).
   * @return This PDF document.
   */
  public XSPDF setTextRenderMode(XSTextRenderingMode textRenderMode) {
    if (textRenderMode == null) {
      textRenderMode = XS.TEXT_FILL;
    }
    currentTextRenderMode = textRenderMode;
    return this;
  }

  /**
   * Returns the render mode that is currently used for printing text.
   * @return Currently used text render mode (stroke, fill, ...).
   */
  public XSTextRenderingMode getTextRenderMode() {
    return currentTextRenderMode;
  }

  /**
   * Set the mode how to deal with spaces in print() texts.
   * @param textFormattingMode Auto format or no formatting.
   * @return This PDF document.
   */
  public XSPDF setTextFormattingMode(XSTextFormattingMode textFormattingMode) {
    if (textFormattingMode == null) {
      textFormattingMode = XSTextFormattingMode.AutoFormat;
    }
    if (currentPage != null && currentPage.currentColumn != null && currentPage.currentColumn.currentTextLine != null
        && currentPage.currentColumn.currentTextLine.hasContent()) {
      throw new XSPdfException("Formatting mode can only be changed for new text lines!");
    }
    currentFormattingMode = textFormattingMode;
    return this;
  }

  /**
   * Returns the current text formatting mode.
   * @return Current text formatting mode.
   */
  public XSTextFormattingMode getTextFormattingMode() {
    return currentFormattingMode;
  }

  /**
   * Set the count of columns with standard margin.
   * @param columnCount Column count.
   * @param columnNamePrefix The name prefix of the columns to create.
   * @return This PDF document.
   */
  public XSPDF addColumns(int columnCount, String columnNamePrefix) {
    return addColumns(columnCount, 1, columnNamePrefix);
  }

  /**
   * Set the count of columns with 1/10 margin.
   * @param columnCount Column count.
   * @param columnRowCount Rows of columns.
   * @param columnNamePrefix The name prefix of the columns to create.
   * @return This PDF document.
   */
  public XSPDF addColumns(int columnCount, int columnRowCount, String columnNamePrefix) {
    if (columnCount < 1) {
      throw new XSPdfException("Column count has to be >= 1!");
    }
    if (columnRowCount < 1) {
      throw new XSPdfException("Column row count has to be >= 1!");
    }

    XSPageSize pageSize = currentPage == null ? currentPageSize : currentPage.pageSize;
    XSPageMargin pageMargin = currentPage == null ? currentPageMargin : currentPage.pageMargin;

    double xMarginSum = pageMargin.left + pageMargin.right + (columnCount - 1) * pageMargin.columnGapX;
    double yMarginSum = pageMargin.top + pageMargin.bottom + (columnRowCount - 1) * pageMargin.columnGapY;
    double columnWidth = (pageSize.width - xMarginSum) / columnCount;
    double columnHeight = (pageSize.height - yMarginSum) / columnRowCount;
    if (columnWidth <= 0 || columnHeight <= 0) {
      throw new XSPdfException("Page too small or pageMargin/gaps too large or too many columns on page!");
    }
    double margin = Math.min(Math.min(pageMargin.left, pageMargin.right), Math.min(pageMargin.top, pageMargin.bottom));
    double dy = pageMargin.top;
    for (int y = 1; y <= columnRowCount; y++) {
      double dx = pageMargin.left;
      for (int x = 1; x <= columnCount; x++) {
        String columnName = columnNamePrefix == null ? null : columnNamePrefix + "_" + y + "_" + x;
        addColumn(dx, dy, columnWidth, columnHeight, columnName, margin * 0.4);
        dx += pageMargin.columnGapX + columnWidth;
      }
      dy += pageMargin.columnGapY + columnHeight;
    }

    return this;
  }

  /**
   * Adds a new column. If there is a "current" page, this column will be added
   * to this page. If not, it will be added as a standard column for all
   * following pages.
   * @param x X coordinate in user units.
   * @param y Y coordinate in user units.
   * @param width Width of the column in user units.
   * @param height Width of the column in user units.
   * @param name Internal column name.
   * @param margin Margin around the column.
   * @return This PDF document.
   */
  public XSPDF addColumn(double x, double y, double width, double height, String name, double margin) {
    if (width < 0) {
      width = -width;
      x -= width;
    }
    if (height < 0) {
      height = -height;
      y -= height;
    }
    x *= currentUnitFactor;
    y *= currentUnitFactor;
    width *= currentUnitFactor;
    height *= currentUnitFactor;
    if (currentPage == null) {
      y = currentPageSize.height - y - height;
      standardColumns.add(new XSColumn(null, x, y, width, height, name, margin, currentColumnBackgroundColor,
          currentLayerID));
    } else {
      currentPage.addColumn(x, currentPage.pageSize.height - y - height, width, height, name, margin,
          currentColumnBackgroundColor, currentLayerID);
    }
    return this;
  }

  /**
   * Set the page margin.
   * @param parm Page margin.
   * @return This PDF document.
   */
  public XSPDF setPageMargin(XSPageMargin parm) {
    currentPageMargin = parm;
    if (currentPage != null) {
      currentPage.setPageMargin(currentPageMargin);
    }
    return this;
  }

  /**
   * Set the page margin.
   * @param margin Margin on every side.
   * @return This PDF document.
   */
  public XSPDF setPageMargin(double margin) {
    setPageMargin(new XSPageMargin(margin * currentUnitFactor));
    return this;
  }

  /**
   * Returns the current page margin.
   * @return Current page margin.
   */
  public XSPageMargin getPageMargin() {
    return currentPageMargin;
  }

  /**
   * Sets the alignment for following printed text.
   * @param alignment Text alignment.
   * @return This PDF document.
   */
  public XSPDF setAlignment(XSAlignment alignment) {
    if (alignment == null) {
      throw new XSPdfException("Can't work without alignment!");
    }
    currentAlignment = alignment;
    return this;
  }

  /**
   * Returns the currently used alignment.
   * @return Currently used alignment.
   */
  public XSAlignment getAlignment() {
    return currentAlignment;
  }

  /**
   * Rotate page
   * @return This PDF document.
   */
  public XSPDF rotatePage() {
    if (currentPage != null) {
      if (currentPage.hasAnyContent()) {
        throw new XSPdfException("Can't rotate page after adding content!");
      }
    }
    currentPageSize = currentPageSize.rotate();
    return this;
  }

  /**
   * Set marker, if the last line in a justified text block should be right
   * aligned or not.
   * @param b Right alignment marker.
   * @return This PDF document.
   */
  public XSPDF setLastJustifiedLineRightAligned(boolean b) {
    lastLineRightAligned = b;
    return this;
  }

  /**
   * Returns marker, if the last line in a justified text block should be right
   * aligned or not.
   * @return Last line right aligned marker.
   */
  public boolean getLastJustifiedLineRightAligned() {
    return lastLineRightAligned;
  }

  /**
   * Set page mode for whole document.
   * @param pageMode A page mode (e.g. FullScreen).
   * @return This PDF document.
   */
  public XSPDF setPageMode(XSPageMode pageMode) {
    this.pageMode = pageMode;
    return this;
  }

  /**
   * Returns the configured page mode.
   * @return Page mode (for whole document).
   */
  public XSPageMode getPageMode() {
    return pageMode;
  }

  /**
   * Set the background color of the page (and following pages).
   * @param color New page background color.
   * @return This PDF document.
   */
  public XSPDF setPageBackgroundColor(Color color) {
    currentPageBackgroundColor = color;
    if (currentPage != null) {
      currentPage.setBackgroundColor(color);
    }
    return this;
  }

  /**
   * Returns the background color of the current page.
   * @return Current page background color.
   */
  public Color getPageBackgroundColor() {
    return currentPageBackgroundColor;
  }

  /**
   * Set the background color of the column (and following columns).
   * @param color New column background color.
   * @return This PDF document.
   */
  public XSPDF setColumnBackgroundColor(Color color) {
    currentColumnBackgroundColor = color;
    if (currentPage != null && currentPage.currentColumn != null) {
      currentPage.currentColumn.setBackgroundColor(color);
    }
    return this;
  }

  /**
   * Returns the background color of the current column.
   * @return Current column background color.
   */
  public Color getColumnBackgroundColor() {
    return currentColumnBackgroundColor;
  }

  /**
   * Set the layer ID for following defined columns.
   * @param layerID The layer ID for the new columns.
   * @return This PDF document.
   */
  public XSPDF setLayerID(int layerID) {
    currentLayerID = layerID;
    return this;
  }

  /**
   * Returns the current layer ID.
   * @return Current layer ID.
   */
  public int getLayerID() {
    return currentLayerID;
  }

  /**
   * Go to the next column.
   * @return This PDF document.
   */
  public XSPDF nextColumn() {
    if (currentPage == null) {
      newPageInternal();
    }
    if (currentPage.currentColumn == null) {
      currentPage.setFirstColumn();
    } else {
      if (currentPage.columns.indexOf(currentPage.currentColumn) + 1 < currentPage.columns.size()) {
        currentPage.setNextColumn();
      } else {
        newPageInternal();
        // Recursion without break condition? No - every page has at least one
        // column!
        nextColumn();
      }
    }
    return this;
  }

  /**
   * Return the current font size in units of 1/72 inch.
   * @return Current font size.
   */
  public double getFontSize() {
    return currentFontSize;
  }

  /**
   * Enable the presentation mode. This means, that fullscreen mode is set and
   * all page durations are set to the given value.
   * @param duration Duration of any page before showing next page.
   * @return This PDF document.
   */
  public XSPDF setPresentationMode(double duration) {
    setPageMode(XSPageMode.FullScreen);
    return setDurationPerPage(duration);
  }

  /**
   * Enable the presentation mode. This means, that fullscreen mode is set and
   * all page durations are set to the given value.
   * @param duration Duration of any page before showing next page.
   * @return This PDF document.
   */
  public XSPDF setDurationPerPage(double duration) {
    if (duration < 0.0) {
      throw new XSPdfException("Duration for presentation mode must be greater or equal to zero!");
    }
    presentationDurationPerPage = duration;
    if (currentPage != null) {
      currentPage.presentationDuration = presentationDurationPerPage;
    }
    return this;
  }

  private XSPDF setTransitionMode(XSTransitionMode transMode) {
    if (transMode == null) {
      transMode = XS.DEFAULT_TRANSITION_MODE;
    }
    currentTransitionMode = transMode;
    if (currentPage != null) {
      currentPage.transitionMode = currentTransitionMode;
    }
    return this;
  }

  /**
   * Set the transition style SPLIT (for current and following pages).
   * @param direction Direction.
   * @param motionDirection Motion direction.
   * @param duration Duration of the effekt in seconds.
   * @return This PDF document.
   */
  public XSPDF setTransitionStyleSplit(XSDirection direction, XSMotionDirection motionDirection, double duration) {
    return setTransitionMode(new XSTransitionMode(XSTransitionStyle.Split, direction, motionDirection, null, duration));
  }

  /**
   * Set the transition style BLINDS (for current and following pages).
   * @param direction Direction.
   * @param duration Duration of the effekt in seconds.
   * @return This PDF document.
   */
  public XSPDF setTransitionStyleBlinds(XSDirection direction, double duration) {
    return setTransitionMode(new XSTransitionMode(XSTransitionStyle.Blinds, direction, null, null, duration));
  }

  /**
   * Set the transition style BOX (for current and following pages).
   * @param motionDirection Motion direction.
   * @param duration Duration of the effekt in seconds.
   * @return This PDF document.
   */
  public XSPDF setTransitionStyleBox(XSMotionDirection motionDirection, double duration) {
    return setTransitionMode(new XSTransitionMode(XSTransitionStyle.Box, null, motionDirection, null, duration));
  }

  /**
   * Set the transition style WIPE (for current and following pages).
   * @param movementDirection Movement direction.
   * @param duration Duration of the effekt in seconds.
   * @return This PDF document.
   */
  public XSPDF setTransitionStyleWipe(XSMovementDirection movementDirection, double duration) {
    return setTransitionMode(new XSTransitionMode(XSTransitionStyle.Wipe, null, null, movementDirection, duration));
  }

  /**
   * Set the transition style DISSOLVE (for current and following pages).
   * @param duration Duration of the effekt in seconds.
   * @return This PDF document.
   */
  public XSPDF setTransitionStyleDissolve(double duration) {
    return setTransitionMode(new XSTransitionMode(XSTransitionStyle.Dissolve, null, null, null, duration));
  }

  /**
   * Set the transition style GLITTER (for current and following pages).
   * @param movementDirection Movement direction.
   * @param duration Duration of the effekt in seconds.
   * @return This PDF document.
   */
  public XSPDF setTransitionStyleGlitter(XSMovementDirection movementDirection, double duration) {
    return setTransitionMode(new XSTransitionMode(XSTransitionStyle.Glitter, null, null, movementDirection, duration));
  }

  /**
   * Set the transition style GLITTER (for current and following pages).
   * @param motionDirection Motion direction.
   * @param movementDirection Movement direction.
   * @param duration Duration of the effekt in seconds.
   * @return This PDF document.
   */
  public XSPDF setTransitionStyleFly(XSMotionDirection motionDirection, XSMovementDirection movementDirection,
      double duration) {
    return setTransitionMode(new XSTransitionMode(XSTransitionStyle.Fly, null, motionDirection, movementDirection,
        duration));
  }

  /**
   * Set the transition style PUSH (for current and following pages).
   * @param movementDirection Movement direction.
   * @param duration Duration of the effekt in seconds.
   * @return This PDF document.
   */
  public XSPDF setTransitionStylePush(XSMovementDirection movementDirection, double duration) {
    return setTransitionMode(new XSTransitionMode(XSTransitionStyle.Push, null, null, movementDirection, duration));
  }

  /**
   * Set the transition style COVER (for current and following pages).
   * @param movementDirection Movement direction.
   * @param duration Duration of the effekt in seconds.
   * @return This PDF document.
   */
  public XSPDF setTransitionStyleCover(XSMovementDirection movementDirection, double duration) {
    return setTransitionMode(new XSTransitionMode(XSTransitionStyle.Cover, null, null, movementDirection, duration));
  }

  /**
   * Set the transition style UNCOVER (for current and following pages).
   * @param movementDirection Movement direction.
   * @param duration Duration of the effekt in seconds.
   * @return This PDF document.
   */
  public XSPDF setTransitionStyleUncover(XSMovementDirection movementDirection, double duration) {
    return setTransitionMode(new XSTransitionMode(XSTransitionStyle.Uncover, null, null, movementDirection, duration));
  }

  /**
   * Set the transition style FADE (for current and following pages).
   * @param duration Duration of the effekt in seconds.
   * @return This PDF document.
   */
  public XSPDF setTransitionStyleFade(double duration) {
    return setTransitionMode(new XSTransitionMode(XSTransitionStyle.Fade, null, null, null, duration));
  }

  /**
   * Set the page label for this page. This method only has to be called when
   * the page numbering of this page is not an increment of the last one.
   * @param labelType Label type.
   * @param prefix Prefix (may be null).
   * @param startValue First value. May be zero to not show any number but only
   *          the prefix.
   * @return This PDF document.
   */
  public XSPDF setPageLabel(XSPageLabelStandardType labelType, String prefix, int startValue) {
    XSPageLabel pageLabel = new XSPageLabel(labelType, prefix, startValue);
    if (currentPage == null) {
      currentPageLabel = pageLabel;
    } else {
      currentPage.pageLabel = pageLabel;
    }
    return this;
  }

  /**
   * Set only a fix string as page label. You have to set the next page label
   * manually to not get the same label as this one for the next page.
   * @param label Page label string.
   * @return This PDF document.
   */
  public XSPDF setPageLabel(String label) {
    return setPageLabel(null, label, 0);
  }

  /**
   * Get the size of the current column.
   * @return Current column size or <i>null</i> if no column currently selected.
   */
  public XSDimension getColumnSize() {
    if (currentPage != null) {
      XSColumn column = currentPage.currentColumn;
      if (column != null) {
        return new XSDimension(column.width, column.height);
      }
    }
    return null;
  }

  /**
   * Starts a link to a destination.
   * @param destinationName Name of the destination.
   * @param count Number of following calls (print() or setImage()) that will
   *          link to this destination. This value may be zero for linking all
   *          texts and images to this destination until stopLink() is called.
   * @return This PDF document.
   */
  public XSPDF startLink(String destinationName, int count) {
    if (count < 0) {
      throw new XSPdfException("Link counter to destination <" + destinationName + "> must not be negative!");
    }
    currentLink = new XSLink(destinationName, currentLinkBorderSize);
    currentLinkCount = count;
    return this;
  }

  /**
   * Starts a link to a destination for a single print() or setImage() call.
   * @param destinationName Name of the destination.
   * @return This PDF document.
   */
  public XSPDF startLink(String destinationName) {
    startLink(destinationName, 1);
    return this;
  }

  /**
   * Starts a link to a URL.
   * @param url URL.
   * @param count Number of following calls (print() or setImage()) that will
   *          link to this destination. This value may be zero for linking all
   *          texts and images to this destination until stopLink() is called.
   * @return This PDF document.
   */
  public XSPDF startLink(URL url, int count) {
    if (count < 0) {
      throw new XSPdfException("Link counter to URL <" + url + "> must not be negative!");
    }
    currentLink = new XSLink(url, currentLinkBorderSize);
    currentLinkCount = count;
    return this;
  }

  /**
   * Starts a link to a URL for a single print() or setImage() call.
   * @param url URL.
   * @return This PDF document.
   */
  public XSPDF startLink(URL url) {
    startLink(url, 1);
    return this;
  }

  /**
   * Stops linking texts or images.
   * @return This PDF document.
   */
  public XSPDF stopLink() {
    currentLink = null;
    currentLinkCount = 0;
    return this;
  }

  /**
   * Set a destination to the current page.
   * @param destinationName Name of the page destination.
   * @return This PDF document.
   */
  public XSPDF setPageDestination(String destinationName) {
    if (destinations.get(destinationName) != null) {
      throw new XSPdfException("The destination name <" + destinationName + "> already exists!");
    }
    int pageID = pages.isEmpty() ? 0 : pages.size() - 1;
    destinations.put(new XSLink(destinationName, -1), new XSPageDestination(pageID));
    return this;
  }

  /**
   * Set a destination to the current line on the current page.
   * @param destinationName Name of the line destination.
   * @return This PDF document.
   */
  public XSPDF setLineDestination(String destinationName) {
    if (destinations.get(destinationName) != null) {
      throw new XSPdfException("The destination name <" + destinationName + "> already exists!");
    }
    if (currentPage == null || currentPage.currentColumn == null) {
      return setPageDestination(destinationName);
    }
    int pageID = pages.isEmpty() ? 0 : pages.size() - 1;
    XSTextLine textline = currentPage.currentColumn.currentTextLine;
    double lineY = currentPage.currentColumn.y;
    if (textline != null) {
      lineY += textline.yStartInColumn;
    }
    destinations.put(new XSLink(destinationName, -1), new XSLineDestination(pageID, lineY));
    return this;
  }

  /**
   * Set the thickness of the border of following links.
   * @param linkBorderSize Thickness of the border of following links.
   * @return This PDF document.
   */
  public XSPDF setLinkBorderSize(double linkBorderSize) {
    if (linkBorderSize < 0) {
      throw new XSPdfException("Thickness of link borders must not be negative!");
    }
    currentLinkBorderSize = linkBorderSize * currentUnitFactor;
    return this;
  }
}
